-
Notifications
You must be signed in to change notification settings - Fork 74
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update UITableView to track changes precisely #1538
Conversation
|
||
override fun remove(index: Int, count: Int) { | ||
val last = edits.lastOrNull() | ||
if (last is Edit.Remove && index in last.index - count until last.index + 1) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to test fencepost logic on these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a drive-by review for now:
This PR changes LazyList from UICollectionView to UITableView. This change was potentially unnecessary, though UITableView has fewer features that we don't need.
Switching it back to UITableView
means that LazyRow
can no longer work (see #942 (comment)). I'd be very hesitant to break support for this!
), | ||
) : WindowedLazyList<UIView>(UICollectionViewListUpdateCallback(collectionView)), ChangeListener { | ||
) : LazyList<UIView>, ChangeListener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal of WindowedLazyList
was to have UIViewLazyList
and ViewLazyList
to share the bulk of their implementations. They are fairly similar (e.g., view based, platform provides a recycling list view that allows atomic updates). The integration of WindowedLazyList
with UIViewLazyList
wasn't complete, namely that UICollectionViewListUpdateCallback
didn't yet delegate to atomic updates.
What are your thoughts on continuing down that path vs switching approach as you did here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's this issue from last week that has more info: #1498
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new class started out as a copy-paste of WindowedLazyList, addressing problems on iOS by adding precise updates.
Are precise updates useful for RecyclerView? If precise updates will make RecyclerView faster, then we should follow-up by migrating RecyclerView to LazyListUpdateProcessor (and delete the old WindowedLazyList). If precise updates are not necessary for RecyclerView, then we should not do precise updates; it’s a bit of work to provide them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reading that issue makes it clear to me that this new thing will benefit both platforms. I’d still like to land it one-platform-at-a-time, mostly because our test coverage here is lacking.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds good. Do you mind adding a TODO
here (or manually create an issue) so that we remember that these two mechanisms should ideally be merged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here’s a tracking issue: #1552
The underlying issue is probably #1500. Increasing to 30 placeholders increases the number of serialized views, which'll be unnecessary for the majority of lists. |
): LazyListContainerCell { | ||
val result = tableView.dequeueReusableCellWithIdentifier( | ||
identifier = reuseIdentifier, | ||
forIndexPath = NSIndexPath.indexPathForItem(index.convert(), 0.convert()), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: is it possible to use the more modern/bridged IndexPath
type here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I couldn’t find it in the Kotlin/Native APIs.
override fun tableView( | ||
tableView: UITableView, | ||
numberOfRowsInSection: NSInteger, | ||
) = processor.size.toLong() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: You might want to add an assertion here that section == 0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
prefetchingEnabled = true | ||
rowHeight = UITableViewAutomaticDimension | ||
estimatedRowHeight = 44.0 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: is this needed? 44 is the default value since iOS 11
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed!
} else { | ||
UICollectionViewScrollDirectionHorizontal | ||
} | ||
// TODO: support horizontal LazyLists. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we plan to support this in the future, or can/should we punt this indefinitely? (Has this already been built on the Android side?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d like to see what happens if I swap out UITableView for UICollectionView. If I’m lucky it’ll just be tracking slight differences in the APIs with no performance impact. I think I’ll be lucky.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dnagler To answer your question directly:
- We plan to support this in the future. We'll need this within Cash for things like the QAB, but also,
LazyRow
should be provided for external consumers (i.e., non Cash people), as these components are supposed to be the lazy counterparts to the existingColumn
/Row
components. - Yes, it's already been built on the Android side.
widgetView?.setFrame(this.contentView.bounds) | ||
|
||
val widgetView = this.widgetView ?: return | ||
widgetView.setFrame(bounds) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: since widgetView
is a subview of contentView
, I would probably do
contentView.setFrame(bounds)
widgetView.setFrame(contentView.bounds)
instead, but it should have the same effect
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'll also definitely want to call widgetView.sizeToFit()
here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After playing with this I’ve learned that contentView is disinterested in delegating to its children for stuff like sizing, so the cell needs to control both. I’m gonna keep it as-is for now ’cause it seems to be working!
Previously we called reloadData() whenever anything changed in the underlying model. With this change we fire precise events to the UI when inserts and removes happen in the source LazyList. This attempts to coalesce changes to itemsBefore() and itemsAfter() with adjacent changes and the beginning and end of the items list. To implement this the changes are buffered into an intermediate model before they are applied. When a cell changes from being a placeholder to a loaded item, or the opposite, this does a cell content change without firing an event through the UITableView. To implement this we must track which cells are currently displaying placeholders. This PR includes a new datastructure, SparseList, to implement this tracking. This PR also changes the number of placeholders that guest code offers to host code. I found experimentally that 20 placeholders was not enough for UITableView. This PR changes LazyList from UICollectionView to UITableView. This change was potentially unnecessary, though UITableView has fewer features that we don't need.
2d86d1d
to
db749b2
Compare
After pairing on this (and seeing marked improvements over current performance!!), I'm in favor of landing this as-is and continuing to iterate, especially since this will be a great help to continuing our QA process on the iOS side (right now our QA team is testing a very broken experience). |
I spent a couple days trying to switch this back to UICollectionView and I’ve been unsuccessful thus far. I can’t figure out how to get dynamic heights working without it exploding because it runs out of placeholders. Could you please release your merge-block on this PR? We can do the UITableView to UICollectionView switch in a follow-up, but I’d like to unblock the stuff we need right now in Cash App. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stamping to unblock
...ood-lazylayout-compose/src/commonMain/kotlin/app/cash/redwood/lazylayout/compose/LazyList.kt
Outdated
Show resolved
Hide resolved
@@ -49,14 +49,14 @@ internal fun LazyList( | |||
val itemsBefore = remember(state.firstVisibleItemIndex) { (state.firstVisibleItemIndex - OffscreenItemsBufferCount / 2).coerceAtLeast(0) } | |||
val itemsAfter = remember(lastVisibleItemIndex, itemProvider.itemCount) { (itemProvider.itemCount - (lastVisibleItemIndex + OffscreenItemsBufferCount / 2).coerceAtMost(itemProvider.itemCount)).coerceAtLeast(0) } | |||
val scrollItemIndex = remember(state.scrollToItemTriggeredId) { ScrollItemIndex(state.scrollToItemTriggeredId, state.firstVisibleItemIndex) } | |||
var placeholderPoolSize by remember { mutableStateOf(20) } | |||
var placeholderPoolSize by remember { mutableStateOf(30) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we add a TODO
to drop this down to 20 again? It's really just blocked on iOS being unable to request more placeholders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
// TODO(jwilson): drop this down to 20 once this is fixed:
// https://github.com/cashapp/redwood/issues/1551
), | ||
) : WindowedLazyList<UIView>(UICollectionViewListUpdateCallback(collectionView)), ChangeListener { | ||
) : LazyList<UIView>, ChangeListener { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds good. Do you mind adding a TODO
here (or manually create an issue) so that we remember that these two mechanisms should ideally be merged?
} else { | ||
UICollectionViewScrollDirectionHorizontal | ||
} | ||
// TODO: support horizontal LazyLists. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dnagler To answer your question directly:
- We plan to support this in the future. We'll need this within Cash for things like the QAB, but also,
LazyRow
should be provided for external consumers (i.e., non Cash people), as these components are supposed to be the lazy counterparts to the existingColumn
/Row
components. - Yes, it's already been built on the Android side.
Previously we called reloadData() whenever anything changed in the underlying model.
With this change we fire precise events to the UI when inserts and removes happen in the source LazyList. This attempts to coalesce changes to itemsBefore() and itemsAfter() with adjacent changes and the beginning and end of the items list. To implement this the changes are buffered into an intermediate model before they are applied.
When a cell changes from being a placeholder to a loaded item, or the opposite, this does a cell content change without firing an event through the UITableView. To implement this we must track which cells are currently displaying placeholders. This PR includes a new datastructure, SparseList, to implement this tracking.
This PR also changes the number of placeholders that guest code offers to host code. I found experimentally that 20 placeholders was not enough for UITableView.
This PR changes LazyList from UICollectionView to
UITableView. This change was potentially unnecessary, though UITableView has fewer features that we don't need.